//  -------------------- iSharePoint -------------------- //
// 
//  Company:	IT Complex, LLC
//  Project:	Smart.Common
//  File name:	Configuration.cs
//  Developer:	Solomatov Igor
//  Created:	15.04.2012
// 
//  -------------------- iSharePoint -------------------- //

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Web.Caching;
using Microsoft.SharePoint;

namespace Smart.Common
{
	/// <summary>
	/// Configuration
	/// </summary>
	[Localizable(false)]
	public class Configuration : IConfiguration
	{
		public SPServicesInstance Services { get; set; }

		public Configuration(SPServicesInstance services)
		{
			Services = services;

			SPServices.Cache.CacheClearing += CacheClearing;
		}

		#region Current properties

		/// <summary>
		/// Detects current site
		/// </summary>
		public virtual SPSite CurrentSite
		{
			get { return SPH.GetSite(); }
		}

		/// <summary>
		/// Detects current web
		/// </summary>
		public virtual SPWeb CurrentWeb
		{
			get { return SPH.GetWeb(); }
		}

		/// <summary>
		/// Detects current list
		/// </summary>
		public virtual SPList CurrentList
		{
			get { return SPH.GetCurrentList(); }
		}

		/// <summary>
		/// Detects current user
		/// </summary>
		public virtual SPUser CurrentUser
		{
			get { return SPH.GetCurrentUser(); }
		}

		#endregion

		/// <summary>
		/// Synchronization lock
		/// </summary>
		private readonly ReaderWriterLockSlim _syncLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);

		#region Get/Set/Remove methods

		/// <summary>
		/// Get Configuration value
		/// </summary>
		/// <param name="key">Configuration key title</param>
		/// <param name="defaultValue">Default configuration value</param>
		/// <returns>Configuration value or defaultValue</returns>
		public virtual string GetValue(string key, string defaultValue)
		{
			return GetValue(CurrentSite, CurrentWeb, CurrentList, CurrentUser, key, defaultValue);
		}

		/// <summary>
		/// Get Configuration Value
		/// </summary>
		/// <param name="site">Current site</param>
		/// <param name="web">Current web</param>
		/// <param name="list">Current list (can be Null)</param>
		/// <param name="user">Current user (can be Null)</param>
		/// <param name="key">Configuration key title</param>
		/// <param name="defaultValue">Default return value</param>
		/// <returns>Configuration value or defaultValue</returns>
		public virtual string GetValue(SPSite site, SPWeb web, SPList list, SPUser user, string key, string defaultValue)
		{
			if (web == null || string.IsNullOrEmpty(key))
				throw new ArgumentNullException();

			_syncLock.EnterUpgradeableReadLock();

			try
			{
				var ck = GetKey(key, site);

				if (ck == null)
					throw new InvalidOperationException();

				ConfigurationValue cv;

				if (user != null)
				{
					cv = GetUserValue(ck, site, user);

					if (cv != null)
					{
						return cv.Value;
					}
				}

				cv = GetMainValue(ck, web, list);

				if (cv != null)
				{
					return cv.Value;
				}
			}
			finally
			{
				_syncLock.ExitUpgradeableReadLock();
			}

			return defaultValue;
		}

		/// <summary>
		/// Set configuration value for user
		/// </summary>
		/// <param name="user">User for which the configuration</param>
		/// <param name="key">Title of configuration</param>
		/// <param name="value">Value of configuration</param>
		public virtual void SetValue(SPUser user, string key, string value)
		{
			if (user == null || string.IsNullOrEmpty(key))
				throw new ArgumentNullException();

			var site = user.ParentWeb.Site;

			_syncLock.EnterWriteLock();

			try
			{
				var ck = GetKey(key, site);

				var subject = GetUserSubject(user.ID);

				var userList = GetUserList(site.RootWeb);
				var userMeta = GetUserMetaList(userList);

				UpdateData(userList, userMeta, ck, subject, value);
			}
			finally
			{
				_syncLock.ExitWriteLock();
			}
		}

		/// <summary>
		/// Remove configuration value for user
		/// </summary>
		public virtual void RemoveValue(SPUser user, string key)
		{
			if (user == null || string.IsNullOrEmpty(key))
				throw new ArgumentNullException();

			var site = user.ParentWeb.Site;

			_syncLock.EnterWriteLock();

			try
			{
				var ck = GetKey(key, site);

				var subject = GetUserSubject(user.ID);

				var userList = GetUserList(site.RootWeb);
				var userMeta = GetUserMetaList(userList);

				UpdateData(userList, userMeta, ck, subject, true);
			}
			finally
			{
				_syncLock.ExitWriteLock();
			}
		}

		/// <summary>
		/// Set configuration value for list
		/// </summary>
		/// <param name="list">List for which the configuration</param>
		/// <param name="key">Title of configuration</param>
		/// <param name="value">Value of configuration</param>
		public virtual void SetValue(SPList list, string key, string value)
		{
			if (list == null || string.IsNullOrEmpty(key))
				throw new ArgumentNullException();

			var site = list.ParentWeb.Site;

			_syncLock.EnterWriteLock();

			try
			{
				var ck = GetKey(key, site);

				var subject = GetListSubject(list);

				var mainList = GetMainList(site.RootWeb);
				var mainMeta = GetMainMetaList(mainList);

				UpdateData(mainList, mainMeta, ck, subject, value);
			}
			finally
			{
				_syncLock.ExitWriteLock();
			}
		}

		/// <summary>
		/// Remove configuration value for list
		/// </summary>
		public virtual void RemoveValue(SPList list, string key)
		{
			if (list == null || string.IsNullOrEmpty(key))
				throw new ArgumentNullException();

			var site = list.ParentWeb.Site;

			_syncLock.EnterWriteLock();

			try
			{
				var ck = GetKey(key, site);

				var subject = GetListSubject(list);

				var mainList = GetMainList(site.RootWeb);
				var mainMeta = GetMainMetaList(mainList);

				UpdateData(mainList, mainMeta, ck, subject, false);
			}
			finally
			{
				_syncLock.ExitWriteLock();
			}
		}

		/// <summary>
		/// Set configuration value for web
		/// </summary>
		/// <param name="web">Web for which the configuration</param>
		/// <param name="key">Title of configuration</param>
		/// <param name="value">Value of configuration</param>
		public virtual void SetValue(SPWeb web, string key, string value)
		{
			if (web == null || string.IsNullOrEmpty(key))
				throw new ArgumentNullException();

			var site = CurrentSite;

			_syncLock.EnterWriteLock();

			try
			{
				var ck = GetKey(key, site);

				var subject = GetWebSubject(web);

				var mainList = GetMainList(site.RootWeb);
				var mainMeta = GetMainMetaList(mainList);

				UpdateData(mainList, mainMeta, ck, subject, value);
			}
			finally
			{
				_syncLock.ExitWriteLock();
			}
		}

		/// <summary>
		/// Remove configuration value for web
		/// </summary>
		public virtual void RemoveValue(SPWeb web, string key)
		{
			if (web == null || string.IsNullOrEmpty(key))
				throw new ArgumentNullException();

			var site = CurrentSite;

			_syncLock.EnterWriteLock();

			try
			{
				var ck = GetKey(key, site);

				var subject = GetWebSubject(web);

				var mainList = GetMainList(site.RootWeb);
				var mainMeta = GetMainMetaList(mainList);

				UpdateData(mainList, mainMeta, ck, subject, false);
			}
			finally
			{
				_syncLock.ExitWriteLock();
			}
		}

		/// <summary>
		/// Set configuration value for current user
		/// </summary>
		/// <param name="key">Title of configuration</param>
		/// <param name="value">Value of configuration</param>
		public virtual void SetUserValue(string key, string value)
		{
			SetValue(CurrentUser, key, value);
		}

		/// <summary>
		/// Remove configuration value for current user
		/// </summary>
		public virtual void RemoveUserValue(string key)
		{
			RemoveValue(CurrentUser, key);
		}

		/// <summary>
		/// Set configuration value for list by list guid
		/// </summary>
		/// <param name="listGuid">List guid for which the configuration</param>
		/// <param name="key">Title of configuration</param>
		/// <param name="value">Value of configuration</param>
		public virtual void SetListValue(Guid listGuid, string key, string value)
		{
			SetValue(CurrentWeb.GetListById(listGuid), key, value);
		}

		/// <summary>
		/// Remove configuration value for list
		/// </summary>
		public virtual void RemoveListValue(Guid listGuid, string key)
		{
			RemoveValue(CurrentWeb.GetListById(listGuid), key);
		}

		/// <summary>
		/// Set configuration value for web by web guid
		/// </summary>
		/// <param name="webGuid">Web guid for which the configuration</param>
		/// <param name="key">Title of configuration</param>
		/// <param name="value">Value of configuration</param>
		public virtual void SetWebValue(Guid webGuid, string key, string value)
		{
			if (webGuid.IsEmpty() || string.IsNullOrEmpty(key))
				throw new ArgumentNullException();

			SetValue(CurrentSite.GetWeb(webGuid), key, value);
		}

		/// <summary>
		/// Remove configuration value for web by web id
		/// </summary>
		public virtual void RemoveWebValue(Guid webGuid, string key)
		{
			if (webGuid.IsEmpty() || string.IsNullOrEmpty(key))
				throw new ArgumentNullException();

			RemoveValue(CurrentSite.GetWeb(webGuid), key);
		}

		#endregion

		#region Caching

		/// <summary>
		/// Clear additional cache
		/// </summary>
		protected virtual void CacheClearing()
		{
			CacheMainConfigurationMeta = null;
			CacheUserConfigurationMeta = null;

			CacheWebGuidSearch.Clear();
		}

		/// <summary>
		/// Returns configuration key cache
		/// </summary>
		/// <param name="key">Configuration key</param>
		/// <returns>Cache entry for ConfigurationKey</returns>
		protected virtual CacheEntry<ConfigurationKey> GetKeyCache(string key)
		{
			return
				new CacheEntry<ConfigurationKey>(Const.Cache.ConfigurationKey, key)
					{
						Priority = CacheItemPriority.High,
						Timeout = SPServices.Cache.LongTimeout,
						SlidingExpiration = SPServices.Cache.ShortTimeout
					};
		}

		protected MetaList CacheMainConfigurationMeta;
		protected MetaList CacheUserConfigurationMeta;

		[MethodImpl(MethodImplOptions.Synchronized)]
		protected virtual MetaList GetMainMetaList(SPList list)
		{
			if (CacheMainConfigurationMeta == null)
			{
				CacheMainConfigurationMeta = new MetaWebList(list, null);

				CacheMainConfigurationMeta.LoadFields(list);
			}

			return CacheMainConfigurationMeta;
		}

		[MethodImpl(MethodImplOptions.Synchronized)]
		protected virtual MetaList GetUserMetaList(SPList list)
		{
			if (CacheUserConfigurationMeta == null)
			{
				CacheUserConfigurationMeta = new MetaWebList(list, null);

				CacheUserConfigurationMeta.LoadFields(list);
			}

			return CacheUserConfigurationMeta;
		}

		protected readonly SyncDictionary<Guid, string[]> CacheWebGuidSearch = new SyncDictionary<Guid, string[]>();

		/// <summary>
		/// Create search region list in reverse order (from web to list)
		/// </summary>
		/// <param name="web">Current web</param>
		/// <param name="list">Current list</param>
		/// <returns>Search region list</returns>
		protected virtual string[] GetGuidList(SPWeb web, SPList list)
		{
			var guidArray = CacheWebGuidSearch.SafeGet(web.ID);

			if (list == null && guidArray != null)
				return guidArray;

			var guidSearch = (guidArray == null ? new List<string>() : new List<string>(guidArray));

			if (guidSearch.Count <= 0)
			{
				guidSearch.Add(web.ID.ToString().ToUpper());

				var currentWeb = web.ParentWeb;

				while (currentWeb != null)
				{
					guidSearch.Add(currentWeb.ID.ToString().ToUpper());

					currentWeb = currentWeb.ParentWeb;
				}

				guidSearch.Add(Const.Configuration.RootSubjectKeyword);

				CacheWebGuidSearch[web.ID] = guidSearch.ToArray();
			}

			if (list != null)
				guidSearch.Add(list.ID.ToString().ToUpper());

			return guidSearch.ToArray();
		}

		#endregion

		#region Helper methods

		/// <summary>
		/// Returns root web id
		/// </summary>
		/// <returns>root web id</returns>
		protected virtual string GetRootSubject(SPSite site)
		{
			return GetWebSubject(site.RootWeb);
		}

		/// <summary>
		/// Return web guid
		/// </summary>
		protected virtual string GetWebSubject(SPWeb web)
		{
			return web.ID.ToString().ToUpper();
		}

		/// <summary>
		/// Return list guid
		/// </summary>
		protected virtual string GetListSubject(SPList list)
		{
			return list.ID.ToString().ToUpper();
		}

		/// <summary>
		/// Return user guid by user id
		/// </summary>
		protected virtual string GetUserSubject(int userId)
		{
			return "U" + userId.ToStringX();
		}

		/// <summary>
		/// Get configuration list
		/// </summary>
		protected virtual SPList GetMainList(SPWeb web)
		{
			return web.GetListByUrl(Const.Configuration.MainListUrl);
		}

		/// <summary>
		/// Get configuration list
		/// </summary>
		protected virtual SPList GetUserList(SPWeb web)
		{
			return web.GetListByUrl(Const.Configuration.UserListUrl);
		}

		/// <summary>
		/// Loads <paramref name="key"/> from configuration lists
		/// </summary>
		/// <param name="key">Configuration key to load</param>
		/// <param name="site">Site to load from</param>
		/// <returns>ConfigurationKey</returns>
		protected virtual ConfigurationKey LoadKey(string key, SPSite site)
		{
			if (key.IsNullOrEmpty()) throw new ArgumentNullException("key");

			var ck = new ConfigurationKey(key);

			var rootSubjectId = GetRootSubject(site);

			// Load key from MainSettings list
			var mainList = GetMainList(site.RootWeb);
			var mainMeta = GetMainMetaList(mainList);

			var mainItems =
				mainList.Query(mainMeta).Where.Eq(SN.TitleField, key).Executor.GetItems();

			ConfigurationValue rootValue = null;

			foreach (SPListItem mainItem in mainItems)
			{
				var subject = mainItem.GetStr(Const.Configuration.SubjectField);
				var value = mainItem.GetStr(Const.Configuration.ValueField);

				if (subject.IsNullOrEmpty())
					continue;

				if (value.Length > Const.Configuration.MaxValueLength)
				{
					value = null;

					H.LX("Configuration.LoadKey", new InvalidOperationException("value.Length should be less then Const.Configuration.MaxValueLength!"));
				}

				var isRootValue = subject.In(Const.Configuration.RootSubjectKeyword, rootSubjectId);

				var cv = new ConfigurationValue(value);

				if (isRootValue)
				{
					rootValue = cv;
				}
				else
				{
					ck.Subjects[subject] = cv;
				}
			}

			if (rootValue == null)
			{
				ck.SubjectsNd.Add(Const.Configuration.RootSubjectKeyword);
				ck.SubjectsNd.Add(rootSubjectId);
			}
			else
			{
				ck.Subjects[Const.Configuration.RootSubjectKeyword] = rootValue;
				ck.Subjects[rootSubjectId] = rootValue;
			}

			return ck;
		}

		/// <summary>
		/// Checks and loads user values for <paramref name="key"/>
		/// </summary>
		/// <param name="key">Configuration key</param>
		/// <param name="userSubjectId">User subject id</param>
		/// <param name="site">Site to load from</param>
		protected virtual void LoadUserKey(ConfigurationKey key, string userSubjectId, SPSite site)
		{
			if (key == null) throw new ArgumentNullException("key");
			if (userSubjectId.IsNullOrEmpty()) throw new ArgumentNullException("userSubjectId");

			// Load key from UserSettings list
			// WARNING! It will and should read ONLY current user settings!
			var userList = GetUserList(site.RootWeb);
			var userMeta = GetUserMetaList(userList);

			var userItems =
				userList.Query(userMeta).Where.Eq(SN.TitleField, key.Name).Eq(Const.Configuration.SubjectField, userSubjectId).Executor.GetItems();

			foreach (SPListItem userItem in userItems)
			{
				var value = userItem.GetStr(Const.Configuration.ValueField);

				if (value.Length > Const.Configuration.MaxValueLength)
				{
					value = null;

					H.LX("Configuration.LoadKey", new InvalidOperationException("value.Length should be less then Const.Configuration.MaxValueLength!"));
				}

				key.Subjects[userSubjectId] = new ConfigurationValue(value);
			}
		}

		/// <summary>
		/// Returns load or cached <paramref name="key"/>
		/// </summary>
		/// <param name="key">Configuration key</param>
		/// <param name="site">Site, used to load key</param>
		/// <returns>ConfigurationKey</returns>
		protected virtual ConfigurationKey GetKey(string key, SPSite site)
		{
			if (site.ID != Services.SiteId)
				throw new InvalidOperationException("Configuration used with different SPSite!");

			var ck = GetKeyCache(key).Value;

			if (ck == null)
			{
				_syncLock.EnterWriteLock();

				try
				{
					var cache = GetKeyCache(key);

					if (cache.Value == null)
					{
						cache.Value = ck = LoadKey(key, site);
					}
				}
				finally
				{
					_syncLock.ExitWriteLock();
				}
			}

			return ck;
		}

		/// <summary>
		/// Gets main value from <paramref name="key"/> for <paramref name="web"/> or <paramref name="list"/>
		/// </summary>
		/// <param name="key">Configuration key</param>
		/// <param name="web">Subject Web</param>
		/// <param name="list">Subject List</param>
		/// <returns>ConfigurationValue</returns>
		protected virtual ConfigurationValue GetMainValue(ConfigurationKey key, SPWeb web, SPList list)
		{
			var subjects = GetGuidList(web, list);

			for (var i = subjects.Length - 1; i >= 0; i--)
			{
				var subject = subjects[i];

				var cv = key.Subjects.SafeGet(subject, (ConfigurationValue)null);

				if (cv != null)
				{
					return cv;
				}
			}

			return null;
		}

		/// <summary>
		/// Gets or loads user value from <paramref name="key"/> for <paramref name="user"/> using <paramref name="site"/>
		/// </summary>
		/// <param name="key">Configuration key</param>
		/// <param name="site">Site to load from</param>
		/// <param name="user">Subject User</param>
		/// <returns>ConfigurationValue</returns>
		protected virtual ConfigurationValue GetUserValue(ConfigurationKey key, SPSite site, SPUser user)
		{
			if (user == null)
				return null;

			ConfigurationValue value = null;

			var userGuid = GetUserSubject(user.ID);

			if (!key.SubjectsNd.Contains(userGuid))
			{
				value = key.Subjects.SafeGet(userGuid, (ConfigurationValue)null);

				if (value == null)
				{
					_syncLock.EnterWriteLock();

					try
					{
						LoadUserKey(key, userGuid, site);

						if (!key.SubjectsNd.Contains(userGuid))
						{
							value = key.Subjects.SafeGet(userGuid, (ConfigurationValue)null);
						}
					}
					finally
					{
						_syncLock.ExitWriteLock();
					}
				}
			}

			return value;
		}

		/// <summary>
		/// Update configuration <paramref name="key"/> with <paramref name="subject"/> and <paramref name="value"/>
		/// </summary>
		/// <param name="key">configuration key to update</param>
		/// <param name="subject">configuration key subject</param>
		/// <param name="value">configuration key value</param>
		protected virtual void SetKeyValue(ConfigurationKey key, string subject, string value)
		{
			key.SubjectsNd.Remove(subject);
			key.Subjects[subject] = new ConfigurationValue(value);
		}

		/// <summary>
		/// Update configuration list
		/// </summary>
		/// <param name="list">Configuration list</param>
		/// <param name="meta">Metadata of configuration list</param>
		/// <param name="key">Configuration key</param>
		/// <param name="subject">Configuration key subject</param>
		/// <param name="value">Configuration key value</param>
		protected virtual void UpdateData(SPList list, MetaList meta, ConfigurationKey key, string subject, string value)
		{
			var item =
				list.Query(meta).Where.Eq(SN.TitleField, key.Name).Eq(Const.Configuration.SubjectField, subject).Executor.GetItem();

			if (item == null)
			{
				item =
					list.AddItem()
						.Set(SN.TitleField, key.Name)
						.Set(Const.Configuration.SubjectField, subject)
						.Set(Const.Configuration.ValueField, value);
			}
			else
			{
				item.Set(Const.Configuration.ValueField, value);
			}

			try
			{
				item.Update();

				SetKeyValue(key, subject, value);
			}
			catch (Exception x)
			{
				H.LX("Configuration.UpdateData", x);
			}
		}

		/// <summary>
		/// Remove from user configuration <paramref name="key"/> value fro <paramref name="subject"/>
		/// </summary>
		/// <param name="key">configuration key to update</param>
		/// <param name="subject">configuration key subject to remove</param>
		/// <param name="addSubjectsNd">Flag to add to Key.SubjectsNd collection</param>
		protected virtual void RemoveKeyValue(ConfigurationKey key, string subject, bool addSubjectsNd)
		{
			if (addSubjectsNd)
				key.SubjectsNd.Add(subject);

			key.Subjects.Remove(subject);
		}

		/// <summary>
		/// Remove from configuration list
		/// </summary>
		/// <param name="list">Configuration list</param>
		/// <param name="meta">Metadata of configuration list</param>
		/// <param name="key">Configuration key</param>
		/// <param name="subject">Configuration key subject to remove</param>
		/// <param name="addSubjectsNd">Flag to add to Key.SubjectsNd collection</param>
		protected virtual void UpdateData(SPList list, MetaList meta, ConfigurationKey key, string subject, bool addSubjectsNd)
		{
			var item =
				list.Query(meta).Where.Eq(SN.TitleField, key.Name).Eq(Const.Configuration.SubjectField, subject).Executor.GetItem();

			try
			{
				if (item != null)
					item.Recycle();

				RemoveKeyValue(key, subject, addSubjectsNd);
			}
			catch (Exception x)
			{
				H.LX("Configuration.UpdateData", x);
			}
		}

		#endregion
	}
}